Skip to content

Add huawei scale 3 support - #1474

Merged
oliexdev merged 11 commits into
oliexdev:masterfrom
Nidala96:add-huawei-scale-3-support
Aug 16, 2026
Merged

oliexdev merged 11 commits into
oliexdev:masterfrom
Nidala96:add-huawei-scale-3-support

Conversation

@Nidala96

Copy link
Copy Markdown
Contributor

Summary

Adds support for Huawei Scale 3 using Huawei's Hagrid/WSP BLE protocol.

The implementation includes:

  • Huawei Hagrid authentication
  • work key exchange and encrypted communication
  • WSP framing and frame reassembly
  • user profile synchronization
  • realtime measurement flow
  • body composition parsing
  • history measurement support
  • Huawei-specific product/profile detection
  • tests for the Hagrid protocol and measurement parsing

Testing

Tested on a physical Huawei Scale 3 with openScale on Android.

Verified:

  • BLE connection
  • Hagrid authentication
  • work key exchange
  • time synchronization
  • manager info read
  • user profile synchronization
  • realtime weight measurements
  • body fat measurements
  • repeated measurements across multiple connections

Implementation notes

Huawei authentication requires CAK, C1 and C2 to be configured manually in the device settings.

The implementation does not include Huawei Health key extraction, APK extraction, root, Frida, or other external key-retrieval mechanisms.

The realtime measurement request is sent before measurement-status polling, matching the behavior observed from the Huawei protocol.

Derived values such as BMR and lean body mass are left to openScale's existing derived-value system instead of being calculated inside the scale driver.

Scope

This PR is focused on Huawei Scale 3 / Hagrid WSP support and does not intentionally change unrelated UI or Bluetooth behavior.

@oliexdev

Copy link
Copy Markdown
Owner

Thanks for your PR.

  1. The new body-composition decode reads the timestamp bytes. Bytes 4–11 of a realtime payload are the timestamp, not muscle/water/bone/visceral. Your own PoC
    capture decrypts to …ea07 08 09 17 27 1d… = 2026-08-09 23:39:29, and historyPayload() is realtimePayload() plus two bytes — where those same bytes are parsed
    via readTimestamp(payload, 4).

The range checks only mask part of it. The PR's own fixture yields visceral=4.2 — that's the seconds field (42) ÷ 10. Systematically: water passes on days 1–3
of any month, bone in minutes 1–3 of any hour, visceral very often. These values get written to the database.

Do you have a capture where those four fields actually carry body composition? Otherwise I'd rather drop this part than write it into users' history.

  1. History sync no longer runs on the happy path. handleMeasurementStatusPayload used to call sendMeasurementDataRequests() on status 0x00; now it only stops
    the polling. That function is reachable only via the poll timeout or a missing poll characteristic, so a scale reporting 0x00 never gets a history read.
    Regression for Scale 2 Pro / Scale 3 Pro — intentional, or a side effect of moving the realtime request into startMeasurementFlow()?

  2. Realtime now publishes unconditionally. The old split looked deliberate: realtime = progress UI, history = stored record (deduped via publishedHistoryKeys).
    The realtime path has no dedupe and falls back to dateTime = Date(), so every notification becomes its own DB row — and with (2) it's the only save path left.
    How many realtime notifications does your Scale 3 send per weighing?

Smaller: the gender flip (MALE→1/FEMALE→0) changes the scale-computed body composition for already-working devices — can you back it with a capture? userType =
0 is the parameter default, so it's a no-op. The four new strings are translatable="false" while comparable ones (s400_bind_key_error) aren't. Plus a few
stray double blank lines.

@Nidala96

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review. You were right on all of those points.

  • I removed the unverified muscle/water/bone/visceral decoding entirely. The realtime parser now treats bytes 4–10 as the timestamp as shown by the capture, and those four fields are no longer written from Hagrid payloads.

  • The missing history read on status 0x00 was an unintended regression from moving the realtime request earlier in the flow. I restored the status=0 path so it now continues with the post-measurement/history reads instead of only stopping the poll.

  • Realtime publishing is no longer unconditional. Existing Hagrid families keep the previous behavior where realtime is UI/progress-only and history is the persistent path.

For Scale 3 specifically, testing on the physical device showed that relying only on the history path did not persist the current weighing. I therefore scoped the realtime persistence fallback to Scale 3 only. It caches the latest valid realtime measurement and publishes at most once after measurement status reaches 0x00. The other Hagrid families are unchanged.

The Scale 3 gender mapping is also now scoped only to Scale 3. I do have a Huawei Health BLE capture for this device showing a male profile encoded with gender byte 0x01. Other Hagrid families retain the previous male=0/female=1 mapping.

I also removed the explicit userType=0 argument and cleaned up the stray formatting.

I also re-tested the current Scale 3 flow on-device and confirmed that the current weighing is persisted correctly.

@oliexdev

Copy link
Copy Markdown
Owner

Thanks! The Scale 3 specifics are cleanly gated on family == SCALE_3 with tests covering
the non-Scale-3 side, so the other Hagrid models keep their behaviour.

One change isn't gated: parseRealtimeMeasurement switching to timestampPresent = true
applies to every family, and it makes the timestamp mandatory
(readTimestamp(payload, 4) ?: return null). If Scale 2 Pro / 3 Pro / HONOR carry something
else at bytes 4-10, realtime parsing returns null for them and the live weight display during
a measurement is gone. Stored data is fine (history has always used timestampPresent = true).
The tests can't catch it — realtimePayload() already contains a valid timestamp in both test
files, which incidentally suggests you're right about the field and master was inconsistent.

Could you make it tolerant so Scale 3 gains the timestamp and nothing else can break?

// realtime: read it when present, but never reject the measurement over it
val timestamp = if (timestampPresent) readTimestamp(payload, 4) else null

@Nidala96

Copy link
Copy Markdown
Contributor Author

Tested the latest changes on a physical Huawei Scale 3 and everything is working correctly.

Realtime timestamp parsing is now tolerant as requested: valid timestamps are kept, while invalid/non-timestamp values no longer reject the realtime measurement. History still requires a valid timestamp.

I also verified the full Scale 3 flow on-device, including realtime updates and persistence of the final measurement.

Thanks again for the detailed review.

- Update test assertions to use Google Truth assertions instead of JUnit assertions
- Map additional scale model names to the correct handler class
- Add mappings for "HUAWEI Scale 3", "HUAWEI Scale 3 Pro", and "Hagrid-B29"
# Conflicts:
#	android_app/app/src/test/java/com/health/openscale/core/bluetooth/ScaleFactoryTest.kt
@oliexdev
oliexdev merged commit d53f165 into oliexdev:master Aug 16, 2026
@oliexdev

Copy link
Copy Markdown
Owner

Thanks — the tolerant realtime timestamp is exactly what I was after, and the separate
timestampRequired flag is nicer than what I sketched: Scale 3 gets its timestamp, no other
family can be rejected over those bytes.

I've merged master and added: Truth assertions in HuaweiHagridWspLibTest, HUAWEI Scale 3
in the registry catalog (so it lands in the generated "Supported scales" table), and a remark
that CAK/C1/C2 have to be entered first.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants